package com.atguigu.gmall.list.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.list.dao.GoodsDao;
import com.atguigu.gmall.list.service.ListService;
import com.atguigu.gmall.model.list.*;
import com.atguigu.gmall.model.product.BaseCategoryView;
import com.atguigu.gmall.model.product.BaseTrademark;
import com.atguigu.gmall.model.product.SkuAttrValue;
import com.atguigu.gmall.model.product.SkuInfo;
import com.atguigu.gmall.product.client.ProductFeignClient;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;


@Service
public class ListServiceImpl implements ListService {
    @Autowired
    public RestHighLevelClient restHighLevelClient;

    @Autowired
    private GoodsDao goodsDao;

    @Autowired
    private ProductFeignClient productFeignClient;

    @Autowired
    private RedisTemplate redisTemplate;

    //导入索引
    @Override
    public void upperGoods(Long skuId) {
        //一部手机全部索引手机
        Goods goods = new Goods();

        //1：根据skuID查询SkuInfo信息
        SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);

        goods.setId(skuInfo.getId());
        goods.setDefaultImg(skuInfo.getSkuDefaultImg());
        goods.setTitle(skuInfo.getSkuName());
        goods.setPrice(skuInfo.getPrice().doubleValue());
        goods.setCreateTime(new Date());

        //2:根据tmId 查询品牌相关数据
        BaseTrademark baseTrademark = productFeignClient.getBaseTrademark(skuInfo.getTmId());
        goods.setTmId(baseTrademark.getId());
        goods.setTmName(baseTrademark.getTmName());
        goods.setTmLogoUrl(baseTrademark.getLogoUrl());


        //3:根据category3Id查询分类视图
        BaseCategoryView baseCategoryView = productFeignClient.getBaseCategoryView(skuInfo.getCategory3Id());
        goods.setCategory1Id(baseCategoryView.getCategory1Id());
        goods.setCategory1Name(baseCategoryView.getCategory1Name());
        goods.setCategory2Id(baseCategoryView.getCategory2Id());
        goods.setCategory2Name(baseCategoryView.getCategory2Name());
        goods.setCategory3Id(baseCategoryView.getCategory3Id());
        goods.setCategory3Name(baseCategoryView.getCategory3Name());


        //4：根据skuId 查询 sku_attr_value   base_attr_info  base_attr_value  三表关联
        List<SkuAttrValue> skuAttrValuesList = productFeignClient.getSkuAttrValue(skuId);
        //private List<SearchAttr> attrs;//平台属性集合  Stream流 偷天换日
        List<SearchAttr> searchAttrList = skuAttrValuesList.stream().map(skuAttrValue -> {
            SearchAttr searchAttr = new SearchAttr();
            searchAttr.setAttrId(skuAttrValue.getBaseAttrInfo().getId());
            searchAttr.setAttrName(skuAttrValue.getBaseAttrInfo().getAttrName());
            searchAttr.setAttrValue(skuAttrValue.getBaseAttrValue().getValueName());
            return searchAttr;
        }).collect(Collectors.toList());
        goods.setAttrs(searchAttrList);

        //保存索引库
        goodsDao.save(goods);


    }

    //删除索引
    @Override
    public void lowerGoods(Long skuId) {

        /*
        //ES官方高级客户端的删除方式
        DeleteRequest deleteRequest = new DeleteRequest();
        deleteRequest.index("goods");
        deleteRequest.id(skuId.toString());
        try {
            restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace();
        }*/


        //企业上班用：SpringDataES的删除方式   与Mybatis-Plus 相同的
        goodsDao.deleteById(skuId);

    }

    @Override
    public void hotScore(Long skuId) {

        //1:通过缓存  进行缓冲    10更新一次ES  五大数据类型
        Double hotScore = redisTemplate.opsForZSet().incrementScore("hotScore", skuId, 1);//1为追加的分数，返回的hotScore为总分

        if (hotScore % 10 == 0) {
            //1:查询ES库 此商品的评分
            Optional<Goods> byId = goodsDao.findById(skuId);
            Goods goods = byId.get();
            //2:将此评分加分
            goods.setHotScore(Math.round(hotScore));//8.0  8
            //3:此商品保存回ES库
            goodsDao.save(goods);
            //ES 添加 修改 删除 速度快吗？ 超慢
            //ES 查询速度超 快
        }
    }


    //开始搜索
    @Override
    public SearchResponseVo search(SearchParam searchParam) {

        SearchResponseVo vo = null;
        try {
            //1:构建搜索请求对象
            SearchRequest searchRequest = buildSearchRequest(searchParam);//构建方法比较负责，单独列出来
            //2:执行搜索
            SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            //3:解析结果对象
            vo = parseSearchResponse(searchResponse);
            //当前页
            vo.setPageNo(searchParam.getPageNo());
            //每页数
            vo.setPageSize(searchParam.getPageSize());
            //总页数 == （总条数 + 每页数 -1)/每页数
            vo.setTotalPages((vo.getTotal() + vo.getPageSize() - 1) / vo.getPageSize());


        } catch (IOException e) {
            e.printStackTrace();
        }

        return vo;
    }

    //抽取 解析结果方法
    private SearchResponseVo parseSearchResponse(SearchResponse searchResponse) {
        //结果对象装值
        SearchResponseVo vo = new SearchResponseVo();
        //解析 商品结果集
        SearchHits hits = searchResponse.getHits();//????

        //创建聚合对象
        Map<String, Aggregation> stringAggregationMap = searchResponse.getAggregations().asMap();//key值就是我们自定义的"aggTmId"，即“父亲”的别名

        //解析平台属性
        ParsedNested aggAttrs = (ParsedNested) stringAggregationMap.get("aggAttrs");//得到大爷辈父亲
        ParsedLongTerms aggAttrId = aggAttrs.getAggregations().get("aggAttrId");//获得父亲的唯一一个儿子，因为是嵌套关系，所以这次只有一个儿子getAggregations()，但是儿子里面有很多孙子,多个才用getBuckets()
        List<SearchResponseAttrVo> attrsList = aggAttrId.getBuckets().stream().map(bucket -> {
            SearchResponseAttrVo attrVo = new SearchResponseAttrVo();
            //①平台属性id——是大爷的大儿子的Id，也是大房中的父亲，即此时bucket是大房内部的父亲
            attrVo.setAttrId(Long.parseLong(bucket.getKeyAsString()));
            //②平台属性名称——获取大房内部的两个儿子
            ParsedStringTerms aggAttrName = ((Terms.Bucket) bucket).getAggregations().get("aggAttrName");
            attrVo.setAttrName(aggAttrName.getBuckets().get(0).getKeyAsString());
            //③平台属性值——多个,是集合，偷天换日
            ParsedStringTerms aggAttrValue = ((Terms.Bucket) bucket).getAggregations().get("aggAttrValue");
            List<String> attrValueList = aggAttrValue.getBuckets().stream().map(Terms.Bucket::getKeyAsString).collect(Collectors.toList());//Terms.Bucket内部类
            attrVo.setAttrValueList(attrValueList);

            return attrVo;
        }).collect(Collectors.toList());

        vo.setAttrsList(attrsList);


        ParsedLongTerms aggTmId = (ParsedLongTerms) stringAggregationMap.get("aggTmId");//拿到一个聚合,强转是因为Aggregation接口方法少，用其实现类方法多
        //List<? extends Terms.Bucket> buckets = aggTmId.getBuckets();//我们目前有这个集合，需要下面的集合，因此继续使用偷天换日的方法进行转换
        //解析品牌集合
        List<SearchResponseTmVo> trademarkList = aggTmId.getBuckets().stream().map(bucket -> {
            SearchResponseTmVo tmVo = new SearchResponseTmVo();
            //父聚合
            tmVo.setTmId(Long.parseLong(bucket.getKeyAsString()));
            //子聚合 品牌名称和品牌Logo
            ParsedStringTerms aggTmName = ((Terms.Bucket) bucket).getAggregations().get("aggTmName");
            tmVo.setTmName(aggTmName.getBuckets().get(0).getKeyAsString());//每一个自聚合中只有一个值，并且getBuckets取出的是一个集合list，其中装了aggTmName，要把他取出来，就需要get(0)
            ParsedStringTerms aggTmLogoUrl = ((Terms.Bucket) bucket).getAggregations().get("aggTmLogoUrl");
            tmVo.setTmLogoUrl(aggTmLogoUrl.getBuckets().get(0).getKeyAsString());

            return tmVo;
        }).collect(Collectors.toList());

        vo.setTrademarkList(trademarkList);

        //商品结果集
        SearchHit[] hits1 = hits.getHits();//?????

        //偷天换日
        List<Goods> goodsList = Arrays.stream(hits1).map(hit -> {
            Goods goods = JSONObject.parseObject(hit.getSourceAsString(), Goods.class);
            //高亮显示设置,解析高亮的值
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            if (highlightFields != null && highlightFields.size()>0){
                HighlightField title = highlightFields.get("title");//高亮域
                if (title!=null){
                    Text[] fragments = title.fragments();//title的内容是一个数组
                    String h = fragments[0].toString();//这个数组只有
                    goods.setTitle(h);
                }
            }

            return goods;
        }).collect(Collectors.toList());

        vo.setGoodsList(goodsList);


        //解析 分页相关信息  总条数
        vo.setTotal(hits.getTotalHits());
        System.out.println(hits.getTotalHits());

        return vo;

    }

    //抽取 构建搜索请求对象
    private SearchRequest buildSearchRequest(SearchParam searchParam) {
        //一、条件对象
        //（二）创建“条件对象”
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //创建当多个条件的时候的组合条件对象
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();//布尔

        /*
         * must：条件必须有，即必须满足 （并且条件之间的关系为and关系）
         * filter： 条件可以有，即过滤条件，页面点不点都可以
         * 但是它们底层都是and的关系，从而实现多条件的组合过滤
         * */
        /*
         * termQuery： 精准查询——不分词查询（如品牌的名字，不能分开，必须精准匹配）
         * matchQuery： 模糊查询——分词查询（如首页搜索“大屏手机”，就会被分为“大屏”和“手机”）
         * */
        //1、第一部分数据参数：“关键词” ——类似于手机、电视
        String keyword = searchParam.getKeyword();
        if (!StringUtils.isEmpty(keyword)) {
            boolQueryBuilder.must(QueryBuilders.matchQuery("title", keyword).operator(Operator.AND));//根据条件查询;operator(Operator.AND)取交集，解决搜索“华为手机”时，出来各种包含华为和单纯包含手机的分词属性
        } else {
            //搜索全部数据
            boolQueryBuilder.must(QueryBuilders.matchAllQuery());//查所有
        }
        //2、第二部分数据参数：“品牌” ——过滤条件   参数形式为——>品牌ID:品牌名称
        String trademark = searchParam.getTrademark();
        if (!StringUtils.isEmpty(trademark)) {
            String[] t = StringUtils.split(trademark, ":");//使用冒号：切分trademark
            boolQueryBuilder.filter(QueryBuilders.termQuery("tmId", t[0]));//精准匹配，由于id和name是绝对绑定的，因此使用id代替name用来比较
        }

        //3、第三部分数据参数：一二三级分类
        Long category1Id = searchParam.getCategory1Id();
        if (category1Id != null) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("category1Id", category1Id));
        }
        Long category2Id = searchParam.getCategory2Id();
        if (category2Id != null) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("category2Id", category2Id));
        }
        Long category3Id = searchParam.getCategory3Id();
        if (category3Id != null) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("category3Id", category3Id));
        }

        //4、第四部分数据参数：平台属性 属性值   参数形式为——>平台属性ID:平台属性值:平台属性名称
        String[] props = searchParam.getProps();
        if (props != null && props.length > 0) {
            //“大爷”级别的父亲
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            //注意此处循环遍历出的是子辈的属性，而不是父辈的
            for (String prop : props) {
                String[] p = prop.split(":");//切割完后数组长度是三个长度，这里不用stringUtils的api是因为，api只能切割两段，而我们这里有三段" xx:xx:xx "
                BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery();//每循环一个子属性，就要放到“大爷”级别的父亲中，即attrs
                subBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", p[0]));
                subBoolQuery.must(QueryBuilders.termQuery("attrs.attrValue", p[1]));

                boolQuery.must(QueryBuilders.nestedQuery("attrs", subBoolQuery, ScoreMode.None));//nestedQuery：嵌套关系的条件查询父与众多的孩子
                //第二个参数？？？？？？？？

            }
            //父辈（大爷辈）属性之间的关系  and关系
            boolQueryBuilder.filter(boolQuery);//value装的是“大爷”级别的父亲
        }

        //（二）构建条件对象 ，只能用一次query方法
        searchSourceBuilder.query(boolQueryBuilder);

        //二、非条件查询
        //6、第六部分数据参数：排序
        //order   1:asc  1:desc  |  2:asc 2:desc | ...
        String order = searchParam.getOrder();
        if (!StringUtils.isEmpty(order)) {
            String[] o = order.split(":");
            String field = null;
            switch (o[0]) {
                case "1":
                    field = "hotScore";
                    break;
                case "2":
                    field = "price";
                    break;
            }
            searchSourceBuilder.sort(field, o[1].equalsIgnoreCase(SortOrder.ASC.toString()) ? SortOrder.ASC : SortOrder.DESC);
        }


        //7、第七部分数据参数：分页
        Integer pageNo = searchParam.getPageNo();
        Integer pageSize = searchParam.getPageSize();
        searchSourceBuilder.from((pageNo - 1) * pageSize);//开始行
        searchSourceBuilder.size(pageSize);


        //三、高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder
                .field("title")
                .preTags("<span style='color:red'>")
                .postTags("</span>");
        searchSourceBuilder.highlighter(highlightBuilder);


        //四、聚合(可以理解为又聚合成多个集合)
        //1、品牌聚合  简单
        //相当于select * from 表 group by tmId
        //"aggTmId"是为了解析取值用的自定义名字
        searchSourceBuilder.aggregation(
                AggregationBuilders.terms("aggTmId").field("tmId").size(80)//父聚合
                        .subAggregation(AggregationBuilders.terms("aggTmName").field("tmName").size(80))//子聚合，子聚合要跟着父聚合走
                        .subAggregation(AggregationBuilders.terms("aggTmLogoUrl").field("tmLogoUrl")));//子聚合，子聚合要跟着父聚合走

        //2、平台属性聚合 难
        searchSourceBuilder.aggregation(
                AggregationBuilders.nested("aggAttrs","attrs")//大爷辈的父亲，nested表示嵌套
                .subAggregation(AggregationBuilders.terms("aggAttrId").field("attrs.attrId")//大爷辈父亲的儿子的Id，同时是下面两个属性的“父辈”
                .subAggregation(AggregationBuilders.terms("aggAttrName").field("attrs.attrName"))//跟随儿子Id的值，也就是“孙子”，但实际是和儿子Id平级
                .subAggregation(AggregationBuilders.terms("aggAttrValue").field("attrs.attrValue"))));//跟随儿子Id的值，也就是“孙子”，但实际是和儿子Id平级


        //打印DSL语句
        System.out.println(searchSourceBuilder.toString());

        //五、本对象只负责请求es，以及指明到哪个索引库去搜索，不负责构建条件，而是由上面的searchSourceBuilder负责
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices("goods");//对应mysql的数据库
        //构建搜索条件对象
        searchRequest.source(searchSourceBuilder);
        return searchRequest;
    }
}
